# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
#     https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
#     https://docs.fastlane.tools/plugins/available-plugins
#

default_platform(:ios)

def current_version_number
  File.read("#{File.dirname(__FILE__)}/../.version").strip
end

# Hack to work around a Fastlane quirk where plugin actions are run from the parent of the current working directory.
# We switch to the first subdirectory, then run the action. The action will run from the parent of the subdirectory, which is
# our actual current working directory. The obvious limitation is that this doesn't work if the current working directory
# does not have any subdirectories.
# This is only needed when our current working directory is not the ./fastlane directory.
# https://docs.fastlane.tools/advanced/fastlane/#directory-behavior
def run_plugin_action_in_current_directory(&block)
  Dir.chdir(Dir["*/"].first.chomp("/"), &block)
end

files_with_version_number = {
  './RevenueCat.podspec' => ['s.version          = "{x}"'],
  './RevenueCatUI.podspec' => ['s.version          = "{x}"'],
  './Sources/Misc/SystemInfo.swift' => ['return "{x}"'],
  './.version' => ['{x}'],
  './scripts/docs/v4_api_migration_guide.html' => ['purchases-ios-docs/{x}/documentation/revenuecat/v4_api_migration_guide'],
  './scripts/docs/v5_api_migration_guide.html' => ['purchases-ios-docs/{x}/documentation/revenuecat/v5_api_migration_guide']
}
PLIST_VERSION_PATTERNS = ["<key>CFBundleShortVersionString</key>\n\t<string>{x}</string>",]
files_with_version_number_without_prerelease_modifiers = {
  './Sources/Info.plist' => PLIST_VERSION_PATTERNS,
  './Tests/BackendIntegrationTestApp/Info.plist' => PLIST_VERSION_PATTERNS,
  './Tests/BackendIntegrationTests/Info.plist' => PLIST_VERSION_PATTERNS,
  './Tests/UnitTests/Info.plist' => PLIST_VERSION_PATTERNS,
  './Tests/UnitTestsHostApp/Info.plist' => PLIST_VERSION_PATTERNS
}

PACKAGE_VERSION_PATTERNS = ["repositoryURL = \"https://github.com/RevenueCat/purchases-ios\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = {x};\n"]
files_to_update_on_latest_stable_releases = {
  './scripts/docs/index.html' => ['purchases-ios-docs/{x}/documentation/revenuecat'],
  './Examples/SampleCat/SampleCat.xcodeproj/project.pbxproj' => PACKAGE_VERSION_PATTERNS
}

REPO_NAME = 'purchases-ios'
SNAPSHOTS_REPO_NAME = 'purchases-ios-snapshots'
PAYWALL_PREVIEW_RESOURCES_REPO_NAME = 'paywall-preview-resources'
CHANGELOG_LATEST_PATH = './CHANGELOG.latest.md'
CHANGELOG_PATH = './CHANGELOG.md'

platform :ios do
  before_all do
    setup_circle_ci

    if ENV['APP_STORE_CONNECT_API_KEY_ISSUER_ID']
      app_store_connect_api_key # This will create a shared client that all the tools know how to use and will look for
    end
  end

  desc "Bump version, update swift header, edit changelog, and create pull request"
  lane :bump do |options|
    bump_version_update_changelog_create_pr(
      current_version: current_version_number,
      changelog_latest_path: CHANGELOG_LATEST_PATH,
      changelog_path: CHANGELOG_PATH,
      files_to_update: files_with_version_number,
      files_to_update_without_prerelease_modifiers: files_with_version_number_without_prerelease_modifiers,
      files_to_update_on_latest_stable_releases: files_to_update_on_latest_stable_releases,
      repo_name: REPO_NAME,
      github_rate_limit: options[:github_rate_limit],
      editor: options[:editor],
      next_version: options[:next_version],
      automatic_release: options[:automatic_release]
    )
    compile_autogenerated_header_commit_and_push(options)
  end

  desc "Automatically bumps version, update swift header, edit changelog, and create pull request"
  lane :automatic_bump do |options|
    next_version, type_of_bump = determine_next_version_using_labels(
      repo_name: REPO_NAME,
      github_rate_limit: options[:github_rate_limit]
    )
    options[:next_version] = next_version
    options[:automatic_release] = true
    if type_of_bump == :skip
      UI.message('Skipping automatic bump since the next version doesn\'t include public facing changes')
      next
    end
    if type_of_bump == :major
      UI.message('Skipping automatic bump since the next version is a major release')
      next
    end
    bump(options)
  end

  desc "Make github release"
  lane :github_release do |options|
    create_github_release(
      version: options[:version],
      repo_name: REPO_NAME,
      github_api_token: ENV["GITHUB_TOKEN"],
      changelog_latest_path: CHANGELOG_LATEST_PATH,
      upload_assets: ['RevenueCat.xcframework.zip', 'RevenueCatUI.xcframework.zip']
    )
  end

  desc "Creates PR changing version to next minor adding a -SNAPSHOT suffix"
  lane :prepare_next_version do |options|
    create_next_snapshot_version(
      current_version: current_version_number,
      repo_name: REPO_NAME,
      github_pr_token: ENV["GITHUB_PULL_REQUEST_API_TOKEN"],
      files_to_update: files_with_version_number,
      files_to_update_without_prerelease_modifiers: files_with_version_number_without_prerelease_modifiers
    )
  end


  desc "Setup development environment"
  lane :setup_dev do |options|
    begin
      sh("brew install swiftlint")
    rescue => exception
      UI.error("❌ Please install homebrew and then re-run this lane: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"")
      raise exception
    end

    pre_commit_link = "../.git/hooks/pre-commit"
    if File.exist?(pre_commit_link) || File.symlink?(pre_commit_link)
      UI.message("pre-commit script already linked ✅")
    else
      UI.message("Linking pre-commit script 🔗")
      Dir.chdir ".." do
        sh("ln -s -f ../../scripts/pre-commit.sh .git/hooks/pre-commit")
      end
    end
  end

  desc "Updates paywall-preview-resources-commit to point to latest commit on main and creates/updates PR"
  lane :update_paywall_preview_resources_commit do |options|
    branch_name = "pw2-update-paywall-preview-resources-commit"

    # Ensure we're starting from an up-to-date main.
    sh("git", "fetch", "origin")

    create_or_checkout_branch(branch_name: branch_name)

    new_hash = sh("git", "ls-remote", "git@github.com:RevenueCat/#{PAYWALL_PREVIEW_RESOURCES_REPO_NAME}.git", "refs/heads/main").split("\t").first
    UI.important("Updating paywall preview resources commit to #{new_hash}")
    update_paywall_preview_resources_commit(new_hash)

    commit_push_and_create_pr_if_necessary(
      repo: REPO_NAME,
      branch_name: branch_name,
      commit_message: "[Automated] Updates commit hash of paywall-preview-resources.",
      title: "[AUTOMATIC][Paywalls V2] Updates commit hash of paywall-preview-resources",
      body: "This is an automatic update of the commit hash of paywall-preview-resources.",
      labels: ["pr:other"],
      team_reviewers: ["coresdk", "monetization"]
    )
  end

  desc "Records Paywall template screenshots and pushes them to the repository at target_repository_path"
  lane :record_and_push_paywall_template_screenshots do |options|
    UI.user_error!("Please provide the target_repository_path, and make sure it is cloned.") unless options[:target_repository_path]

    this_commit_hash = sh("git", "rev-parse", "HEAD").strip

    target_repository_path = Dir.chdir("..") do
      File.expand_path(options[:target_repository_path])
    end
    target_repository_branch = "update-ios-template-screenshots"
    target_repository_name = nil

    # Update the target repository and create a new branch
    Dir.chdir(target_repository_path) do
      UI.user_error!("Target repository (#{target_repository_path}) is not clean.") unless sh("git", "status", "--porcelain").empty?
      sh("git", "fetch")
      sh("git", "checkout", "main")
      sh("git", "pull")

      run_plugin_action_in_current_directory do
        create_or_checkout_branch(branch_name: target_repository_branch)
      end
      target_repository_name = sh("basename -s .git \"$(git config --get remote.origin.url)\"").strip
    end
    UI.user_error!("Failed to determine target repository name") if target_repository_name.nil? || target_repository_name.empty?

    # Loop over all platforms and generate screenshots for each
    platforms = ["ios",
                # These platforms work locally but we have been unable to figure out how to get them to work on CI due to codesigning issues.
                #  "iphone-app-on-mac",
                #  "ipad-app-on-mac",
                 "mac-catalyst-optimized-for-mac",
                 "mac-catalyst-scaled-to-match-ipad",
                 "mac-native"]

    platforms.each do |platform|
      UI.message("Generating screenshots for platform: #{platform}")

      output_dir = record_paywall_screenshots(platform: platform)
      UI.message("Output directory for #{platform}: #{output_dir}")

      # Process the screenshots from the output directory
      process_paywall_screenshots(
        output_dir: output_dir,
        target_repository_path: target_repository_path,
        platform: platform
      )
    end

    # Commit, push, and create a PR in the target repository
    Dir.chdir(target_repository_path) do
      commit_push_and_create_pr_if_necessary(
        repo: target_repository_name,
        branch_name: target_repository_branch,
        commit_message: "[Automated] Updates template screenshots for all Apple platforms (commit: #{this_commit_hash})",
        title: "[AUTOMATIC] Updates template screenshots for all Apple platforms",
        body: "This is an automatic update of the template screenshots for iOS, iPhone app on Mac, iPad app on Mac, Mac Catalyst optimized for Mac, and Mac Catalyst scaled to match iPad platforms.",
        team_reviewers: ["coresdk", "monetization"]
      )
    end
  end


  private_lane :process_paywall_screenshots do |options|
    output_dir = options[:output_dir] || UI.user_error!("No output_dir provided")
    target_repository_path = options[:target_repository_path] || UI.user_error!("No target_repository_path provided")
    platform = options[:platform] || UI.user_error!("No platform provided")

    Dir.glob("#{output_dir}/images/*.png").each do |file_path|
      offering_id = File.basename(file_path, File.extname(file_path))
      target_dir = "#{target_repository_path}/screenshots/templates/#{offering_id}"

      target_path = "#{target_dir}/#{platform}.png"

      UI.message("Moving #{file_path} to #{target_path}")

      FileUtils.mkdir_p(target_dir)
      FileUtils.cp(file_path, target_path)
    end

    # Upload snapshots to Emerge
    UI.message("Uploading snapshots to Emerge for platform: #{platform}")
    sh("bundle", "exec", "emerge", "upload", "snapshots",
       "--name", "RevenueCat",
       "--id", "com.revenuecat.paywall-rendering-validation.#{platform}",
       "--repo-name", "RevenueCat/purchases-ios",
       "#{output_dir}/images")
  end

  private_lane :commit_push_and_create_pr_if_necessary do |options|
    branch_name = options[:branch_name] || UI.user_error!("No branch_name provided")
    commit_message = options[:commit_message] || UI.user_error!("No commit_message provided")
    title = options[:title] || commit_message
    body = options[:body] || commit_message
    repo = options[:repo] || REPO_NAME
    labels = options[:labels] || []
    team_reviewers = options[:team_reviewers] || []

    has_changes = !sh("git", "status", "--porcelain").empty?
    if has_changes
      commit_and_push(
        branch_name: branch_name,
        commit_message: commit_message
      )

      run_plugin_action_in_current_directory do
        create_pr_if_necessary(
          github_pr_token: ENV["GITHUB_TOKEN"],
          repo_name: repo,
          base_branch: "main",
          head_branch: branch_name,
          title: title,
          body: body,
          labels: labels,
          team_reviewers: team_reviewers
        )
      end
    else
      UI.message("No changes detected. Skipping commit, push, and PR creation.")
    end
  end

  private_lane :commit_and_push do |options|
    branch_name = options[:branch_name] || UI.user_error!("No branch_name provided")
    commit_message = options[:commit_message] || UI.user_error!("No commit_message provided")

    # Check if there are any changes to commit
    if sh("git", "status", "--porcelain").empty?
      UI.message("No changes to commit")
    else
      # Commit changes
      if File.basename(Dir.pwd) == "fastlane"
        Dir.chdir("..") do
          sh("git", "add", ".")
          sh("git", "commit", "-m", commit_message)
          sh("git", "push", "-u", "origin", branch_name)
        end
      else
        sh("git", "add", ".")
        sh("git", "commit", "-m", commit_message)
        sh("git", "push", "-u", "origin", branch_name)
      end
    end
  end

  desc "Records screenshots for paywalls"
  lane :record_paywall_screenshots do |options|
    platform = options[:platform] || UI.user_error!("No platform provided. Use one of: ios, iphone-app-on-mac, ipad-app-on-mac, mac-catalyst-optimized-for-mac, mac-catalyst-scaled-to-match-ipad")

    # Map platform to destination
    destinations = {
      "ios" => "platform=iOS Simulator,name=iPhone 16",
      "iphone-app-on-mac" => "platform=macOS,arch=arm64,variant=Designed for iPhone",
      "ipad-app-on-mac" => "platform=macOS,arch=arm64,variant=Designed for iPad",
      "mac-catalyst-optimized-for-mac" => "platform=macOS,arch=arm64,variant=Mac Catalyst",
      "mac-catalyst-scaled-to-match-ipad" => "platform=macOS,arch=arm64,variant=Mac Catalyst",
      "mac-native" => "platform=macOS,arch=arm64"
    }

    # Map platform to TARGETED_DEVICE_FAMILY build setting. This overrides the setting in the Xcode Project,
    # if we ever change the list of supported platforms we'll need to make sure to update this as well.
    targeted_device_families = {
      "mac-catalyst-optimized-for-mac" => "1,2,3,4,6",
      "mac-catalyst-scaled-to-match-ipad" => "1,2,3,4"
    }

    destination = destinations[platform]
    UI.user_error!("Invalid platform '#{platform}'. Use one of: #{destinations.keys.join(', ')}") unless destination

    output_dir = File.absolute_path("../fastlane/test_output/xctest/paywall-screenshots")
    FileUtils.rm_rf(output_dir)
    FileUtils.mkdir_p(output_dir)

    result_bundle_path = "#{output_dir}/RevenueCatUITests.xcresult"

    xcargs = []
    if targeted_device_families[platform]
      xcargs += ["TARGETED_DEVICE_FAMILY='#{targeted_device_families[platform]}'"]
    end

    Dir.chdir("../Tests/UnitTestsHostApp/fastlane") do
      match(type: "development", readonly: true)
    end

    scan_options = {
      workspace: 'RevenueCat.xcworkspace',
      scheme: 'RevenueCatUITestsDev',
      testplan: 'Paywall-Screenshots',
      destination: destination,
      result_bundle: true,
      result_bundle_path: result_bundle_path,
    }

    # Add xcargs only if we have build settings to apply
    scan_options[:xcargs] = xcargs.join(' ') unless xcargs.empty?

    scan(scan_options)

    image_output_dir = "#{output_dir}/images"
    FileUtils.mkdir_p(image_output_dir)
    extract_xcresult_images(
      xcresult_path: result_bundle_path,
      output_dir: image_output_dir
    )

    output_dir
  end

  desc "Build the Paywalls Test app for Emerge Snapshots"
  lane :build_paywalls_tester_for_emerge do |options|
    build_paywalls_tester_for_emerge_ios(options)
    # November 2025: Emerge snapshots for these platforms are flaky, disabling for now.
    # build_paywalls_tester_for_emerge_mac(platform: "mac-catalyst-scaled-to-match-ipad")
    # build_paywalls_tester_for_emerge_mac(platform: "mac-native")
    # build_paywalls_tester_for_emerge_mac(platform: "mac-catalyst-optimized-for-mac")


    # As of July 2025, Emerge Tools does not support iOS apps on Mac yet.
    # build_paywalls_tester_for_emerge_mac(platform: "ipad-app-on-mac")
    # build_paywalls_tester_for_emerge_mac(platform: "iphone-app-on-mac")
  end

  lane :build_paywalls_tester_for_emerge_ios do |options|
    scan(
      workspace: "Tests/TestingApps/PaywallsTester/PaywallsTester.xcworkspace",
      scheme: "PaywallsTester - Live Config",
      build_for_testing: true,
    )

    send_to_emerge(
        app_id_suffix: ""
    )
  end

  private_lane :build_paywalls_tester_for_emerge_mac do |options|
    platform = options[:platform]

    # As of July 2025, Emerge Tools has explicitly enabled Catalyst support for apps with the bundle identifiers com.revenuecat.PaywallsTester-<platform> for these 4 platform values.
    # If you change one of these values, you will need to verify that Catalyst support has been enabled for all bundle identifiers, or tell them the new bundle identiifer.
    case platform
    when "mac-catalyst-scaled-to-match-ipad"
      build_mac_catalyst_version(platform: platform, device_families: "1,2,3,4,5,7")
    when "mac-catalyst-optimized-for-mac"
      build_mac_catalyst_version(platform: platform, device_families: "1,2,3,4,5,6,7")
    when "ipad-app-on-mac"
      build_ios_app_on_mac_version(platform: platform, include_ipad: true)
    when "iphone-app-on-mac"
      build_ios_app_on_mac_version(platform: platform, include_ipad: false)
    when "mac-native"
      build_mac_native_version(platform: platform)
    else
      UI.user_error!("Invalid platform: #{platform}. Must be one of: mac-catalyst-scaled-to-match-ipad, mac-catalyst-optimized-for-mac, ipad-app-on-mac, iphone-app-on-mac")
    end
  end

  private_lane :build_mac_catalyst_version do |options|
    platform = options[:platform]
    device_families = options[:device_families]
    xcargs = "CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO"
    xcargs << " TARGETED_DEVICE_FAMILY='#{device_families}'"

    # Remove macosx from SUPPORTED_PLATFORMS in project.pbxproj. xcodebuild will fail if macosx is in the list, even
    # if we override it in xcargs.
    project_file_path = File.absolute_path("../Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj")
    file_content = File.read(project_file_path)
    modified_content = file_content.gsub(/SUPPORTED_PLATFORMS = "[^"]*";/, 'SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";')
    File.write(project_file_path, modified_content)

    archive_path = File.absolute_path("build/PaywallsTester-#{platform}.xcarchive")

    gym(
      workspace: "Tests/TestingApps/PaywallsTester/PaywallsTester.xcworkspace",
      scheme: "PaywallsTester - Live Config",
      configuration: "Debug",
      destination: "platform=macOS,arch=arm64,variant=Mac Catalyst",
      xcargs: xcargs,
      skip_codesigning: true,
      archive_path: archive_path,
      skip_package_pkg: true,
      skip_package_ipa: false
    )

    send_to_emerge(
        app_id_suffix: platform,
        archive_path: archive_path
    )
  ensure
    # Reset project file changes
    sh("git checkout -- ../Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj")
  end

  private_lane :build_mac_native_version do |options|
    platform = options[:platform]
    xcargs = "CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO"

    archive_path = File.absolute_path("build/PaywallsTester-#{platform}.xcarchive")

    gym(
      workspace: "Tests/TestingApps/PaywallsTester/PaywallsTester.xcworkspace",
      scheme: "PaywallsTester - Live Config",
      configuration: "Debug",
      destination: "platform=macOS,arch=arm64",
      xcargs: xcargs,
      skip_codesigning: true,
      archive_path: archive_path,
      skip_package_pkg: true,
      skip_package_ipa: false
    )

    send_to_emerge(
        app_id_suffix: platform,
        archive_path: archive_path
    )
  end

  private_lane :build_ios_app_on_mac_version do |options|
    platform = options[:platform]
    include_ipad = options[:include_ipad]

    # Build iOS version first with appropriate device family targeting
    device_families = include_ipad ? "1,2" : "1"
    xcargs = "CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO"
    xcargs << " TARGETED_DEVICE_FAMILY='#{device_families}'"

    ios_archive_path = File.absolute_path("build/PaywallsTester-#{platform}-ios.xcarchive")

    # Build the iOS version
    gym(
      workspace: "Tests/TestingApps/PaywallsTester/PaywallsTester.xcworkspace",
      scheme: "PaywallsTester - Live Config",
      configuration: "Debug",
      destination: "generic/platform=iOS",
      xcargs: xcargs,
      archive_path: ios_archive_path,
      skip_codesigning: true,
      skip_package_pkg: true,
      skip_package_ipa: true
    )

    # Extract the .app file from the archive
    ios_app_path = "#{ios_archive_path}/Products/Applications/PaywallsTester.app"

    unless File.exist?(ios_app_path)
      UI.user_error!("Could not find iOS .app at expected path: #{ios_app_path}")
    end

    # Create the Mac app wrapper structure
    mac_app_path = File.absolute_path("build/PaywallsTester-#{platform}.app")
    wrapper_dir = "#{mac_app_path}/Wrapper"
    wrapped_app_path = "#{wrapper_dir}/PaywallsTester.app"
    symlink_path = "#{mac_app_path}/WrappedBundle"

    # Clean up any existing structure
    FileUtils.rm_rf(mac_app_path) if File.exist?(mac_app_path)

    # Create the directory structure
    FileUtils.mkdir_p(wrapper_dir)

    # Copy the iOS .app to the Wrapper directory
    UI.message("Copying iOS .app from #{ios_app_path} to #{wrapped_app_path}")
    FileUtils.cp_r(ios_app_path, wrapped_app_path)

    # Create the symlink
    UI.message("Creating symlink from #{symlink_path} to Wrapper/PaywallsTester.app")
    File.symlink("Wrapper/PaywallsTester.app", symlink_path)

    # Verify the structure was created correctly
    unless File.exist?(wrapped_app_path) && File.symlink?(symlink_path)
      UI.user_error!("Failed to create proper Mac app structure")
    end

    UI.success("Successfully created Mac app structure at #{mac_app_path}")

    send_to_emerge(
        app_id_suffix: platform,
        file_path: mac_app_path
    )
  end

  private_lane :send_to_emerge do |options|
    pr_url = ENV["CIRCLE_PULL_REQUEST"]
    branch_name = sh("git rev-parse --abbrev-ref HEAD").strip
    pr_number = nil
    # Try to parse the PR number from the pr_url variable first
    if pr_url && pr_url.include?("/")
      pr_number = pr_url.split("/").last
    # Try to parse the PR number from the merge queue branch name. In this case,
    # there's no CIRCLE_PULL_REQUEST environment variable.
    elsif branch_name =~ /^gh-readonly-queue\/[^\/]+\/pr-([0-9]+)-[0-9a-f]+$/
      pr_number = $1
    end

    emerge_args = {repo_name: "RevenueCat/purchases-ios"}
    emerge_args[:pr_number] = pr_number if pr_number
    emerge_args[:app_id_suffix] = options[:app_id_suffix]

    # Add emerge_config.yaml to .xcarchive files
    if options[:archive_path] && options[:archive_path].end_with?('.xcarchive')
      emerge_config_source = File.absolute_path("../emerge/emerge_config.yaml")
      emerge_config_dest = "#{options[:archive_path]}/emerge_config.yaml"

      if File.exist?(emerge_config_source)
        FileUtils.cp(emerge_config_source, emerge_config_dest)
        UI.message("Added emerge_config.yaml to #{File.basename(options[:archive_path])}")
      else
        UI.important("emerge_config.yaml not found at #{emerge_config_source}")
      end

      emerge_args[:file_path] = options[:archive_path]
    elsif options[:archive_path] || options[:file_path]
      emerge_args[:file_path] = options[:archive_path] || options[:file_path]
    end

    emerge(emerge_args)
  end

  desc "Runs all the iOS tests"
  lane :test_ios do |options|
    generate_snapshots = ENV["CIRCLECI_TESTS_GENERATE_SNAPSHOTS"] == "true"

    # For platforms that fail to run StoreKit tests.
    # See https://developer.apple.com/forums/thread/724068 and `FB12223404`.
    skip_sk_tests = options[:skip_sk_tests]

    if generate_snapshots
      if skip_sk_tests
        test_plan = "CI-RevenueCat-Snapshots"
      else
        test_plan = "CI-Snapshots"
      end
    elsif skip_sk_tests
      test_plan = "CI-RevenueCat"
    else
      test_plan = "CI-AllTests"
    end

    scan_with_flaky_test_retries(
      step_name: "scan - iPhone",
      device: ENV['SCAN_DEVICE'] || "iPhone 12 (15.2)",
      ensure_devices_found: true,
      scheme: "RevenueCat",
      prelaunch_simulator: true,
      output_types: 'junit',
      result_bundle: true,
      testplan: test_plan,
      configuration: 'Debug',
      output_directory: "fastlane/test_output/xctest/ios",
      number_of_retries: generate_snapshots ? 0 : 5, # Number of retries in an xcodebuild run
      fail_build: !generate_snapshots,
      number_of_flaky_retries: generate_snapshots ? 0 : 5 # Number of retries in an xcodebuild run
    )
  end

  desc "Runs all the tvOS tests"
  lane :test_tvos do |options|
    scan_with_flaky_test_retries(
      step_name: "scan - Apple TV",
      device: ENV['SCAN_DEVICE'] || "Apple TV",
      ensure_devices_found: true,
      scheme: "RevenueCat",
      prelaunch_simulator: true,
      output_types: 'junit',
      number_of_retries: 5, # Number of retries in an xcodebuild run
      result_bundle: true,
      testplan: "CI-AllTests",
      configuration: 'Debug',
      output_directory: "fastlane/test_output/xctest/tvos",

      number_of_flaky_retries: 5 # Number of xcodebuild runs to retry
    )
  end

  desc "Runs all the watchOS tests"
  lane :test_watchos do |options|
    generate_snapshots = ENV["CIRCLECI_TESTS_GENERATE_SNAPSHOTS"] == "true"
    if generate_snapshots
       test_plan = "CI-RevenueCat-Snapshots"
    else
      test_plan = "CI-RevenueCat"
    end

    scan_with_flaky_test_retries(
      step_name: "scan - watchOS",
      device: ENV['SCAN_DEVICE'] || "Apple Watch Series 10 (46mm)",
      ensure_devices_found: true,
      scheme: "RevenueCat",
      prelaunch_simulator: true,
      output_types: 'junit',
      result_bundle: true,
      # Host app isn't available on watchOS
      testplan: test_plan,
      configuration: 'Debug',
      output_directory: "fastlane/test_output/xctest/watchos",
      number_of_retries: generate_snapshots ? 0 : 5, # Number of retries in an xcodebuild run
      fail_build: !generate_snapshots,

      number_of_flaky_retries: generate_snapshots ? 0 : 5 # Number of retries in an xcodebuild run
    )
  end

  desc "Runs all RevenueCatUI tests"
  lane :test_revenuecatui do |options|
    generate_snapshots = ENV["CIRCLECI_TESTS_GENERATE_REVENUECAT_UI_SNAPSHOTS"] == "true"
    platform = ENV['PLATFORM'] || 'iOS Simulator'
    destination = ENV['DEVICE'] || "iPhone 14,OS=16.4"
    sdk = ENV['BUILD_SDK'] || 'iphonesimulator'

    fetch_snapshots

    begin
      # See https://forums.swift.org/t/i-made-a-little-spm-package-that-uses-uikit-how-can-i-run-tests-from-command-line/50399/2
      xcodebuild(
        workspace: '.',
        scheme: 'RevenueCatUI',
        destination: "platform=" + platform + ",name=" + destination,
        sdk: sdk,
        output_style: :basic,
        result_bundle_path: 'fastlane/test_output/revenuecatui.xcresult',
        report_formats: [:junit],
        report_path: 'fastlane/test_output/revenuecatui/tests.xml',
        test: true,
        xcargs: generate_snapshots ? "-testPlan CI-Snapshots" : "-testPlan CI-RevenueCatUI"
      )
    rescue => e
      # Equivalent to `fail_build: !generate_snapshots`
      raise e if !generate_snapshots
    end
  end

  desc "Release checks"
  lane :release_checks do |options|
    version_number = current_version_number
    check_no_git_tag_exists(version_number)
    check_pods
    export_xcframeworks
    check_no_github_release_exists(version_number)
  end

  desc "build tvOS, watchOS, macOS"
  lane :build_tv_watch_mac do |options|
    load_spm_dependencies
    build_release(platform: 'watchOS')
    build_release(platform: 'tvOS')
    build_release(platform: 'macOS')
  end

  desc "macOS build"
  lane :build_mac do |options|
    load_spm_dependencies
    build_release(platform: 'macOS')
  end

  desc "visionOS build"
  lane :build_visionos do |options|
    build_release(
      platform: 'visionOS',
      scheme: 'RevenueCat'
    )
    build_release(
      platform: 'visionOS',
      scheme: 'RevenueCatUI'
    )
  end

  desc "archive"
  lane :archive do
    match(type: "appstore", readonly: true)
    gym(export_method: "app-store")
  end

  desc "archive all platforms"
  lane :archive_all_platforms do
    platforms = {
      'ios' => 'generic/platform=ios',
      'tvos' => 'generic/platform=tvos',
      'catalyst' => 'generic/platform=macOS,variant=Mac Catalyst'
    }

    platforms.each do |platform, destination|
      match(type: "appstore", platform: platform, readonly: true)
      gym(export_method: "app-store", destination: destination)
    end
  end

  desc "run API Tests"
  lane :run_api_tests do
    # note: it's called "run" tests but these tests only need to build to run
    workspace_path = 'RevenueCat-Tuist.xcworkspace'
    unless File.directory?(workspace_path)
      UI.important("#{workspace_path} not found. Run `tuist generate --no-open` before invoking this lane.")
    end
    xcodebuild(
      workspace: workspace_path,
      scheme: 'All API Tests',
      destination: 'generic/platform=iOS Simulator',
      configuration: 'release',
    )
  end

  desc "Setup XCConfig file for E2E tests"
  private_lane :setup_xcconfig do |options|
    api_key = options[:api_key] || UI.user_error!("api_key is required")
    force_server_error_strategy = options[:force_server_error_strategy] || "never"
    api_host = options[:api_host] || ""
    proxy_url_scheme = options[:proxy_url_scheme] || ""
    proxy_url_host = options[:proxy_url_host] || ""

    base_path = "#{Dir.pwd}/.."
    xcconfig_path = "#{base_path}/CI.xcconfig"

    UI.message("Creating XCConfig file at #{xcconfig_path}")

    content = [
      "// Auto-generated. Do not commit to VCS",
      "// Generated on #{Time.now}",
      "",
      "REVENUECAT_API_KEY = #{api_key}",
      "REVENUECAT_FORCE_SERVER_ERROR_STRATEGY = #{force_server_error_strategy}"
    ]

    content << "REVENUECAT_API_HOST = #{api_host}" unless api_host.empty?
    content << "REVENUECAT_PROXY_URL_SCHEME = #{proxy_url_scheme}" unless proxy_url_scheme.empty?
    content << "REVENUECAT_PROXY_URL_HOST = #{proxy_url_host}" unless proxy_url_host.empty?

    File.write(xcconfig_path, content.join("\n"))
    UI.success("✅ Created CI.xcconfig")
  end

  # Runs Maestro E2E tests.
  # Parameters:
  #   - backend_environment: ["production", "load_shedder_us_east_1", "load_shedder_us_east_2", "fallback"]
  #   - store: ["app_store", "test_store"]
  #
  # Note that currently the "test_store" store is only available in the production environment.
  desc "run_maestro_e2e_tests"
  lane :run_maestro_e2e_tests do |options|
    backend_environment = options[:backend_environment] || "production"
    store = options[:store] || "app_store"
    base_path = "#{Dir.pwd}/.."

    UI.message("Running Maestro E2E tests for backend environment: #{backend_environment}")

    # Get API key from environment variable
    api_key = ENV["REVENUECAT_API_KEY"]
    UI.user_error!("REVENUECAT_API_KEY environment variable must be set") unless api_key

    # Determine configuration based on backend environment
    api_host = ""
    force_server_error_strategy = "never"
    entitlement_verification_result = ""
    customer_info_source = ""
    heartbeat_monitor_ping_url = ""

    case backend_environment
    when "load_shedder_us_east_1"
      api_host = "fortress-us-east-1.revenuecat.com"
      entitlement_verification_result = "verified"
      customer_info_source = "load_shedder"
      heartbeat_monitor_ping_url = ENV["HEARTBEAT_MONITOR_URL_E2E_TESTS_LOAD_SHEDDER_US_EAST_1"]
    when "load_shedder_us_east_2"
      api_host = "fortress-us-east-2.revenuecat.com"
      entitlement_verification_result = "verified"
      customer_info_source = "load_shedder"
      heartbeat_monitor_ping_url = ENV["HEARTBEAT_MONITOR_URL_E2E_TESTS_LOAD_SHEDDER_US_EAST_2"]
    when "fallback"
      force_server_error_strategy = "primary_backend_down"
      entitlement_verification_result = "verified_on_device"
      customer_info_source = "offline_entitlements"
      heartbeat_monitor_ping_url = ENV["HEARTBEAT_MONITOR_URL_E2E_TESTS_FALLBACK"]
    when "production"
      entitlement_verification_result = "verified"
      customer_info_source = "main"
      heartbeat_monitor_ping_url = ENV["HEARTBEAT_MONITOR_URL_E2E_TESTS_PRODUCTION"]
    else
      UI.user_error!("Invalid backend_environment: #{backend_environment}. Must be one of: production, load_shedder_us_east_1, load_shedder_us_east_2, fallback")
    end

    # Set environment variables for Maestro
    ENV["MAESTRO_EXPECTED_VERIFICATION_RESULT"] = entitlement_verification_result
    ENV["MAESTRO_EXPECTED_CUSTOMER_INFO_SOURCE"] = customer_info_source
    ENV["MAESTRO_STORE"] = store

    UI.message("Setting Maestro environment variables:")
    UI.message("  MAESTRO_EXPECTED_VERIFICATION_RESULT: #{entitlement_verification_result}")
    UI.message("  MAESTRO_EXPECTED_CUSTOMER_INFO_SOURCE: #{customer_info_source}")

    # Setup XCConfig with the appropriate configuration
    setup_xcconfig(
      api_key: api_key,
      api_host: api_host,
      force_server_error_strategy: force_server_error_strategy
    )

    # note: it's called "run" tests but these tests only need to build to run
    xcworkspace = 'RevenueCat-Tuist.xcworkspace'
    unless File.directory?(xcworkspace)
      UI.important("#{xcworkspace} not found. Run `tuist generate --no-open` before invoking this lane.")
    end

    xcodebuild(
      workspace: xcworkspace,
      scheme: "Maestro",
      sdk: "iphonesimulator",
      configuration: "Debug",
      derivedDataPath: "./build"
    )

    # Install app to simulator
    UI.message("Installing app to simulator...")
    sh("xcrun", "simctl", "install", "booted", "#{base_path}/build/Build/Products/Debug-iphonesimulator/Maestro.app")

    if store == "app_store"
      bundle_id = "com.revenuecat.maestro.ios"
      storekit_config_path = "#{base_path}/Examples/rc-maestro/rc-maestro/Resources/StoreKit/app787ddb07e6.storekit"

      # Start file watcher in background
      UI.message("Starting StoreKit config file watcher...")
      watcher_script = "#{base_path}/fastlane/storekit_config_file_watcher.rb"
      watcher_pid = Process.spawn(
        "ruby", watcher_script,
        bundle_id, storekit_config_path
      )

      # Give watcher time to start
      sleep 1
      UI.success("✓ StoreKit config watcher started (PID: #{watcher_pid})")
    end

    success = false
    begin
      # Construct the flow directory path
      flow_dir = "#{base_path}/Examples/rc-maestro/maestro/e2e_tests"

      # Verify the flow directory exists
      unless File.directory?(flow_dir)
        UI.user_error!("Flow directory not found: #{flow_dir}")
      end

      UI.message("Using flow directory: #{flow_dir}")

      sh("maestro", "test", "--format", "junit", "--output", "test_output/maestro/report.xml", "--test-output-dir", "test_output/maestro", flow_dir)

      success = true
    rescue => e
      raise e
    ensure
      slack_backend_integration_test_results(
        environment: backend_environment,
        success: success,
        message_binary_solo_on_failure: false
      )
      ping_heartbeat_monitor(url: heartbeat_monitor_ping_url)

      if store == "app_store"
        # Always stop the watcher, even if Maestro fails
        UI.message("Stopping StoreKit config watcher...")
        Process.kill('TERM', watcher_pid) rescue nil
        Process.wait(watcher_pid) rescue nil
        UI.success("✓ StoreKit config watcher stopped")
      end
    end
  end

  desc "replace API KEY for installation and integration tests"
  lane :replace_api_key_integration_tests do
    replace_text_in_files(
      previous_text: "REVENUECAT_API_KEY",
      new_text: ENV["REVENUECAT_API_KEY"],
      paths_of_files_to_update: [
        './Tests/BackendIntegrationTests/Constants.swift',
        './Tests/InstallationTests/CommonFiles/RCInstallationRunner.m'
      ]
    )
    replace_text_in_files(
      previous_text: "REVENUECAT_LOAD_SHEDDER_API_KEY",
      new_text: ENV["REVENUECAT_LOAD_SHEDDER_API_KEY"],
      paths_of_files_to_update: [
        './Tests/BackendIntegrationTests/Constants.swift',
        './Tests/v3LoadShedderIntegration/v3LoadShedderIntegrationTests/V3LoadShedderIntegrationTests.swift',
        './Tests/v4LoadShedderIntegration/v4LoadShedderIntegrationTests/V4LoadShedderIntegrationTests.swift'
      ]
    )
    replace_text_in_files(
      previous_text: "REVENUECAT_CUSTOM_ENTITLEMENT_COMPUTATION_API_KEY",
      new_text: ENV["REVENUECAT_CUSTOM_ENTITLEMENT_COMPUTATION_API_KEY"],
      paths_of_files_to_update: [
        './Tests/BackendIntegrationTests/Constants.swift',
        './Tests/InstallationTests/SPMCustomEntitlementComputationInstallation/InstallationTests.swift'
      ]
    )

    replace_text_in_files(
      previous_text: "REVENUECAT_PROXY_URL",
      new_text: ENV["REVENUECAT_PROXY_URL"].to_s,
      paths_of_files_to_update: [
        './Tests/BackendIntegrationTests/Constants.swift',
        './Tests/InstallationTests/CommonFiles/RCInstallationRunner.m',
        './Tests/InstallationTests/SPMCustomEntitlementComputationInstallation/InstallationTests.swift'
      ],
      allow_empty: true
    )

    replace_text_in_files(
      previous_text: "REVENUECAT_API_BASE_URL",
      new_text: ENV["REVENUECAT_API_BASE_URL"].to_s,
      paths_of_files_to_update: [
        './Tests/BackendIntegrationTests/Constants.swift'
      ],
      allow_empty: true
    )
  end

  desc "Create XCFramework, and create GitHub release"
  lane :release do |options|
    version_number = current_version_number
    export_xcframeworks
    github_release(version: version_number)
  end

  desc "Release to CocoaPods"
  lane :push_revenuecat_pod do |options|
    pod_push_with_error_handling(path: "RevenueCat.podspec", synchronous: true)
  end

  desc "Release to CocoaPods"
  lane :push_revenuecatui_pod do |options|
    pod_push_with_error_handling(path: "RevenueCatUI.podspec", synchronous: true)
  end

  desc "Tag current branch with current version number"
  lane :tag_current_branch do |options|
    version_number = current_version_number
    check_no_git_tag_exists(version_number)

    add_git_tag(tag: version_number)
    push_git_tags(tag: version_number)
  end

  desc "Trigger bump"
  lane :trigger_bump do
    trigger_action_in_circle_ci(action: 'bump', repo_name: REPO_NAME)
  end

  desc "Checks whether the RevenueCat-Swift.h is up to date"
  private_lane :check_autogenerated_header_up_to_date do |options|
    ensure_git_status_clean
    compile_autogenerated_header
    begin
      ensure_git_status_clean
    rescue => e
      UI.error("RevenueCat-Swift.h autogenerated file changed. Make sure it's up-to-date before releasing.")
    end
  end

  private_lane :compile_autogenerated_header_commit_and_push do |options|
    ensure_git_status_clean
    compile_autogenerated_header
    begin
      ensure_git_status_clean
    rescue => e
      commit_current_changes(commit_message: "Update RevenueCat-Swift.h for version #{current_version_number}")
      push_to_git_remote
    end
  end

  desc "Trigger CircleCI job to generate snapshots for RevenueCat"
  lane :generate_snapshots_RC do
    generate_snapshots(
      pipeline: "generate_snapshots"
    )
  end

  desc "Trigger CircleCI job to generate snapshots for RevenueCatUI"
  lane :generate_snapshots_RCUI do
    generate_snapshots(
      pipeline: "generate_revenuecatui_snapshots"
    )
  end

  private_lane :build_release do |options|
    platform = options[:platform]
    scheme = options[:scheme] || 'RevenueCat'

    xcodebuild(
      workspace: 'RevenueCat.xcworkspace',
      scheme: scheme,
      destination: "generic/platform=#{platform}",
      configuration: 'release',
      xcargs: "CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES"
    )
  end

  private_lane :generate_snapshots do |options|
    require 'rest-client'

    # Prompt branch
    default_branch = git_branch
    branch = UI.input("Branch (defaults to #{default_branch}): ")
    branch = default_branch if branch == ""

    # Get CircleCI token
    circle_token = ENV["CIRCLE_TOKEN"]
    UI.user_error! "Please set the CIRCLE_TOKEN environment variable" unless circle_token

    # Make request
    headers = {"Circle-Token": circle_token, "Content-Type": "application/json", "Accept": "application/json"}
    data = {parameters: {options[:pipeline] => true}, branch: branch}
    url = "https://circleci.com/api/v2/project/github/RevenueCat/#{REPO_NAME}/pipeline"

    resp = RestClient.post url, data.to_json, headers

    # Print workflow url
    number = JSON.parse(resp.body)["number"]
    workflow_url = "https://app.circleci.com/pipelines/github/RevenueCat/#{REPO_NAME}/#{number}"

    UI.important "Workflow: #{workflow_url}"
  end

  desc "Creates a new PR after new snapshot files were generated"
  lane :create_snapshot_pr do |options|
    push_snapshot_pr(
      version: options[:version],
      repo: "revenuecat/#{REPO_NAME}",
      base_branch: ENV["CIRCLE_BRANCH"],
      files_to_add: "../*/__Snapshots__/*"
     )
  end

  desc "Creates a new PR on purchases-ios-snapshots after new snapshot files were generated"
  lane :create_snapshots_repo_pr do |options|
    Dir.chdir("../Tests/#{SNAPSHOTS_REPO_NAME}") { |dir|
     push_snapshot_pr(
      version: options[:version],
      repo: "revenuecat/#{SNAPSHOTS_REPO_NAME}",
      base_branch: "main",
      files_to_add: "."
     )
    }
  end

  private_lane :push_snapshot_pr do |options|
    version = options[:version]
    base_branch = options[:base_branch]
    build_number = ENV["CIRCLE_BUILD_NUM"]

    branch_name = "generated_snapshots/#{base_branch}-#{build_number}-#{version}"

    sh("git", "checkout", "-b", branch_name)
    sh("git", "add", options[:files_to_add])

    file_count = sh("git diff --cached --numstat | wc -l").strip.to_i

    if file_count == 0
      UI.important("No files to be committed")
    else
      sh("git", "commit", "-m", "[skip ci] Generating new test snapshots")
      sh("git", "push", "origin", branch_name)

      circle_user = ENV["CIRCLE_USERNAME"]
      branch_link = "https://github.com/RevenueCat/purchases-ios/tree/#{base_branch}"
      body = "Requested by @#{circle_user} for [#{base_branch}](#{branch_link})"

      create_pull_request(
        repo: options[:repo],
        title: "Generating new test snapshots for `#{base_branch}` - #{version}",
        body: body,
        base: base_branch,
        api_token: ENV["GITHUB_TOKEN"],
        head: branch_name,
        labels: ["test"]
      )
    end
  end

  desc "Creates RevenueCat-Swift.h for a new release"
  lane :compile_autogenerated_header do |options|
    # See https://github.com/RevenueCat/purchases-ios/blob/main/Documentation.docc/V4_API_Migration_guide.md#objc--spm

    require 'fileutils'

    arch = "x86_64"

    Dir.mktmpdir {|dir|
      xcodebuild(
        scheme: 'RevenueCat',
        workspace: 'RevenueCat.xcworkspace',
        derivedDataPath: dir,
        configuration: 'release',
        arch: arch,
        build_settings: {
          'ONLY_ACTIVE_ARCH': 'YES'
        }
      )

      FileUtils.cp(
        "#{dir}/Build/Intermediates.noindex/RevenueCat.build/Release/RevenueCat.build/Objects-normal/#{arch}/RevenueCat-Swift.h",
        File.absolute_path("../Tests/InstallationTests/CommonFiles/RevenueCat-Swift.h")
      )
    }
  end

  lane :export_xcframeworks do |options|
    create_and_sign_xcframework(
      scheme: 'RevenueCat',
      product_name: 'RevenueCat',
      platforms: [
      'iOS',
      'macOS',
      'maccatalyst',
      'tvOS',
      'watchOS',
      'visionOS'
    ])
    create_and_sign_xcframework(
      scheme: 'RevenueCatUIDev',
      product_name: 'RevenueCatUI',
      platforms: [
      'iOS',
      'maccatalyst',
      'watchOS',
    ])
  end

  private_lane :create_and_sign_xcframework do |options|
    scheme = options[:scheme]
    product_name = options[:product_name]
    platforms = options[:platforms]

    output_directory = "build/xcframeworks/#{product_name}"

    create_xcframework(
      destinations: platforms,
      scheme: scheme,
      product_name: product_name,
      xcframework_output_directory: output_directory,
      # Bitcode is no longer required since Xcode 14.
      enable_bitcode: false,
      include_debug_symbols: true,
      # Set SKIP_INSTALL and BUILD_LIBRARY_FOR_DISTRIBUTION.
      # See https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle#Set-up-the-project
      override_xcargs: true
    )

    # sh runs from the Fastfile's location, but other commands run from the project root.
    output_directory_for_sh = "../#{output_directory}"

    # Post-process headers to suppress StoreKit 1 deprecation warnings
    suppress_deprecation_warnings_in_headers(
      xcframework_path: "#{output_directory_for_sh}/#{product_name}.xcframework"
    )

    # Sign XCFramework for Xcode 15
    sign_xcframework(
      file: "#{output_directory_for_sh}/#{product_name}.xcframework"
    )

    xcframeworks_zip_path_for_sh = "../#{product_name}.xcframework.zip"
    sh("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent",
      output_directory_for_sh,
      xcframeworks_zip_path_for_sh)

  end

  desc "Run BackendIntegrationTests"
  lane :backend_integration_tests do |options|
    fail_build = options[:fail_build]

    fetch_snapshots
    replace_api_key_integration_tests

    success = false
    begin
      scan_with_flaky_test_retries(
        scheme: "BackendIntegrationTests",
        ensure_devices_found: true,
        derived_data_path: "scan_derived_data",
        output_types: 'junit',
        output_style: 'raw',
        result_bundle: true,
        testplan: options[:test_plan],
        configuration: 'Debug',
        output_directory: "fastlane/test_output/xctest/ios",

        number_of_flaky_retries: 5 # Number of retries in an xcodebuild run
      )
      success = true
    rescue => exception
      raise exception
    ensure
      if options[:test_plan] == "BackendIntegrationTests-All-CI"
        # In the All CI backend integration test plan the load shedder tests are ran against us-east-1 and us-east-2 and all the other tests are ran against production
        slack_backend_integration_test_results(
          environment: "production, load_shedder_us_east_1, load_shedder_us_east_2, fallback",
          success: success,
          message_binary_solo_on_failure: false
        )
        ping_heartbeat_monitor(url: ENV['HEARTBEAT_MONITOR_URL_BACKEND_INTEGRATION_TESTS'])
      end
    end
  end

  desc "Run LoadShedder tests in old major"
  lane :loadshedder_integration_tests_in_old_major do |options|
    replace_api_key_integration_tests
    major = options[:major] || UI.user_error!("Please provide :major (Integer, e.g., 3 or 4)")
    major = major.to_i
    UI.user_error!("Unsupported major version: #{major}. Supported: 3 or 4") unless [3, 4].include?(major)
    success = false
    begin
      scan(
        project: "./Tests/v#{major}LoadShedderIntegration/v#{major}LoadShedderIntegration.xcodeproj",
        device: ENV['SCAN_DEVICE'] || "iPhone 16 (18.5)",
        scheme: "v#{major}LoadShedderIntegration",
        ensure_devices_found: true,
        derived_data_path: "scan_derived_data",
        output_types: 'junit',
        output_style: 'raw',
        result_bundle: true,
        configuration: 'Debug',
        output_directory: "fastlane/test_output/xctest/ios"
      )
      success = true
    rescue => exception
      raise exception
    ensure
      slack_backend_integration_test_results(
        environment: "load_shedder_us_east_1",
        success: success,
        version: major.to_s,
        message_binary_solo_on_failure: false
      )
      ping_heartbeat_monitor(url: ENV["HEARTBEAT_MONITOR_URL_LOAD_SHEDDER_V#{major}_INTEGRATION_TESTS"])
    end
  end


  desc "Update swift package commit"
  lane :update_swift_package_commit do
    project_file_locations = [
      './Tests/InstallationTests/SPMInstallation/SPMInstallation.xcodeproj/project.pbxproj',
      './Tests/InstallationTests/SPMCustomEntitlementComputationInstallation/SPMInstallation.xcodeproj/project.pbxproj',
      './Tests/InstallationTests/ReceiptParserInstallation/ReceiptParserInstallation.xcodeproj/project.pbxproj',
      './Examples/MagicWeather/MagicWeather.xcodeproj/project.pbxproj',
      './Examples/MagicWeatherSwiftUI/Magic Weather SwiftUI.xcodeproj/project.pbxproj'
    ]

    old_kind_line = "kind = branch;"
    new_kind_line = "kind = revision;"

    commit_hash = last_git_commit[:commit_hash]
    old_branch_line = "branch = main;"
    new_revision_line = "revision = #{commit_hash};"

    replace_text_in_files(
      previous_text: old_kind_line,
      new_text: new_kind_line,
      paths_of_files_to_update: project_file_locations
    )

    replace_text_in_files(
      previous_text: old_branch_line,
      new_text: new_revision_line,
      paths_of_files_to_update: project_file_locations
    )
  end

  private_lane :suppress_deprecation_warnings_in_headers do |options|
    xcframework_path = options[:xcframework_path]

    # Find all RevenueCat-Swift.h files in the XCFramework
    swift_headers = Dir.glob("#{xcframework_path}/**/RevenueCat-Swift.h")

    swift_headers.each do |header_path|
      UI.message("Processing header: #{header_path}")

      content = File.read(header_path)

      # Check if pragma directives are already present
      unless content.include?('#pragma clang diagnostic ignored "-Wdeprecated-declarations"')
        # Add pragma directives to suppress StoreKit 1 deprecation warnings
        pragma_insertion_point = content.index('#pragma clang diagnostic ignored "-Wgcc-compat"')

        if pragma_insertion_point
          insertion_index = content.index("\n", pragma_insertion_point) + 1
          pragma_directives = <<~PRAGMA
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
          PRAGMA

          content.insert(insertion_index, pragma_directives)
          File.write(header_path, content)
          UI.success("Added deprecation warning suppression to #{File.basename(header_path)}")
        else
          UI.error("Could not find insertion point in #{File.basename(header_path)}")
        end
      else
        UI.message("Deprecation warnings already suppressed in #{File.basename(header_path)}")
      end
    end
  end

  private_lane :sign_xcframework do |options|
    match(
      readonly: true,
      git_url: ENV['CERTS_REPO_URL'],
      type: 'appstore',
      skip_provisioning_profiles: true,
      # This isn't important, we just need to fetch the certificate
      app_identifier: "com.revenuecat.CarthageIntegration"
    )

    sh(
      "codesign",
      "--timestamp",
      "-v",
      "--sign", "Apple Distribution: RevenueCat, Inc. (8SXR2327BM)",
      options[:file]
    )
  end

  private_lane :build_symbols_for_docs do
    ios_version = ENV['DOCS_IOS_VERSION'] || "16.1"

    sh("swift",
       "build",
       "--target", "RevenueCat",
       # Build for iOS instead of the default macOS. This ensures that iOS-only symbols are included in the docs.
       "-Xswiftc", "-sdk", "-Xswiftc", sh("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path").strip!,
       "-Xswiftc", "-target", "-Xswiftc", "arm64-apple-ios#{ios_version}-simulator",
       "-Xswiftc", "-emit-symbol-graph",
       "-Xswiftc", "-emit-symbol-graph-dir",
       "-Xswiftc", ".build")
  end

  desc "Preview docs"
  lane :preview_docs do
    ios_version = ENV['DOCS_IOS_VERSION'] || "16.1"

    ENV["INCLUDE_DOCC_PLUGIN"] = "true"
    Dir.chdir("..") do
      build_symbols_for_docs

      sh("swift",
         "package",
         "--disable-sandbox",
         "preview-documentation",
         "--target",
         "RevenueCat",
         "--platform",
         "name=iOS,version=#{ios_version}",
         "--transform-for-static-hosting",
         "--enable-inherited-docs",
         "--additional-symbol-graph-dir", ".build")
    end
  end

  desc "Generate docs without publishing"
  lane :build_docs do
    build_docs_shared
  end

  desc "Generate & publish docs"
  lane :build_and_publish_docs do
    docs_generation_folder = build_docs_shared

    version_number = current_version_number
    docs_repo_base_url = ENV["DOCS_REPO_BASE_URL"]
    docs_repo_name = ENV["DOCS_REPO_NAME"]

    UI.user_error!("Missing environment variable: DOCS_REPO_BASE_URL") unless docs_repo_base_url
    UI.user_error!("Missing environment variable: DOCS_REPO_NAME") unless docs_repo_name

    docs_repo_url = File.join(docs_repo_base_url, docs_repo_name)

    docs_files = Dir.glob("../scripts/docs/*.html").map { |file| File.realpath(file) }
    # clone docs repo
    Dir.mktmpdir do |docs_repo_clone_dir|
      Dir.chdir(docs_repo_clone_dir) do
        sh("git", "clone", docs_repo_url)
        Dir.chdir(docs_repo_name) do
          # copy docs generated in the previous step into the docs folder
          # and push the changes
          docs_destination_folder = "docs/#{version_number}"
          FileUtils.cp_r docs_generation_folder + "/.", docs_destination_folder
          docs_files.each do |file|
            FileUtils.cp file, "docs/"
          end

          # using sh instead of fastlane commands because fastlane would run
          # from the repo root
          sh("git", "add", docs_destination_folder)
          sh("git", "add", "docs/*.html")
          sh("git", "commit", "-m", "Update documentation for #{version_number}")
          sh("git", "push")
        end
      end
    end
  end

  desc "Builds the docs"
  private_lane :build_docs_shared do
    ENV["INCLUDE_DOCC_PLUGIN"] = "true"

    version_number = current_version_number
    docs_repo_name = ENV["DOCS_REPO_NAME"]
    ios_version = ENV['DOCS_IOS_VERSION'] || "16.1"

    UI.user_error!("Missing environment variable: DOCS_REPO_NAME") unless docs_repo_name

    hosting_base_path = File.join(docs_repo_name, version_number)

    docs_generation_folder = Dir.mktmpdir

    # swift package must be run from the dir that contains the Package.swift
    # output is generated in docs_generation_folder
    Dir.chdir("..") do
      build_symbols_for_docs

      sh("swift",
          "package",
          "--disable-sandbox",
          "--allow-writing-to-directory",
          docs_generation_folder,
          "generate-documentation",
          "--target",
          "RevenueCat",
          "--disable-indexing", # Produces a DocC archive that is best-suited for hosting online but incompatible with Xcode.
          "--platform",
          "name=iOS,version=#{ios_version}",
          "--output-path",
          docs_generation_folder,
          "--hosting-base-path",
          hosting_base_path,
          "--transform-for-static-hosting",
          "--enable-inherited-docs",
          "--additional-symbol-graph-dir", ".build")
    end

    docs_generation_folder # returning path from lane
  end

  desc "Build and deploy PurchaseTesterSwiftUI"
  lane :deploy_purchase_tester do |options|
    dry_run = options[:dry_run] || false

    project_root = File.expand_path("#{File.dirname(__FILE__)}/..")

    latest_changelog = File.read("#{project_root}/#{CHANGELOG_LATEST_PATH}")
    changelog = "Release #{current_version_number}:\n\n#{latest_changelog}"

    purchase_tester_root = "#{project_root}/Tests/TestingApps/PurchaseTesterSwiftUI"
    xcodeproj_path = "#{purchase_tester_root}/PurchaseTester.xcodeproj"

    # The following commands use configuration that lives in the PurchaseTesterSwiftUI directory,
    # like the Matchfile. So we move to that project's directory to run them.
    Dir.chdir("#{purchase_tester_root}/fastlane") do
      match(readonly: true, platform: "ios")
      increment_build_number(
        build_number: latest_testflight_build_number(platform: 'ios') + 1,
        xcodeproj: xcodeproj_path,
      )
    end

    build_ios_app(
      workspace: 'RevenueCat.xcworkspace',
      scheme: "PurchaseTester",
      destination: "generic/platform=ios",
      xcodebuild_formatter: '',
      export_method: 'app-store',
      export_options: {
        manageAppVersionAndBuildNumber: true,
        provisioningProfiles: {
          "com.revenuecat.sampleapp" => "match AppStore com.revenuecat.sampleapp",
          "com.revenuecat.sampleapp.watchkitapp" => "match AppStore com.revenuecat.sampleapp.watchkitapp"
        }
      }
    )

    # Wait 20 minutes for the build to be processed by Apple otherwise fail
    wait_processing_timeout_duration_in_seconds = 60 * 20

    if not dry_run
      upload_to_testflight(
      changelog: changelog,
      app_identifier: "com.revenuecat.sampleapp",
      wait_processing_timeout_duration: wait_processing_timeout_duration_in_seconds
    )
    else
      UI.message("Dry run mode enabled. Skipping upload to TestFlight")
    end

    # The following commands use configuration that lives in the PurchaseTesterSwiftUI directory,
    # like the Matchfile. So we move to that project's directory to run them.
    Dir.chdir("#{purchase_tester_root}/fastlane") do
      match(readonly: true, platform: "macos", additional_cert_types: "mac_installer_distribution")
      increment_build_number(
        build_number: latest_testflight_build_number(platform: 'osx') + 1,
        xcodeproj: xcodeproj_path,
      )
    end

    build_mac_app(
      workspace: 'RevenueCat.xcworkspace',
      scheme: "PurchaseTester",
      destination: 'generic/platform=macOS',
      xcodebuild_formatter: '',
      export_method: 'app-store',
      export_options: {
        manageAppVersionAndBuildNumber: true,
        provisioningProfiles: {
          "com.revenuecat.sampleapp" => "match AppStore com.revenuecat.sampleapp macos",
        }
      }
    )

    if not dry_run
      upload_to_testflight(
        changelog: changelog,
        app_platform: 'osx',
        app_identifier: "com.revenuecat.sampleapp",
        wait_processing_timeout_duration: wait_processing_timeout_duration_in_seconds
      )
    else
        UI.message("Dry run mode enabled. Skipping upload to TestFlight")
    end

  end

  desc "Clones or updates snapshots repo"
  lane :fetch_snapshots do
    Dir.chdir("../Tests") do
      if !File.directory?(SNAPSHOTS_REPO_NAME)
        UI.important("Cloning snapshots repo")

        sh "git", "clone", "git@github.com:RevenueCat/#{SNAPSHOTS_REPO_NAME}.git"
      end

      commit = current_snapshots_commit
      UI.message("Updating snapshots repo to #{commit}")
      Dir.chdir(SNAPSHOTS_REPO_NAME) do
        sh "git", "fetch", "origin"
        sh "git", "checkout", commit
      end
    end
  end

  desc "Clones or updates paywall-preview-resources repo"
  lane :fetch_paywall_preview_resources do
    Dir.chdir("../Tests") do
      if !File.directory?(PAYWALL_PREVIEW_RESOURCES_REPO_NAME)
        UI.important("Cloning paywall-preview-resources repo")
        sh "git", "clone", "git@github.com:RevenueCat/#{PAYWALL_PREVIEW_RESOURCES_REPO_NAME}.git"
      end

      commit = current_paywall_preview_resources_commit
      UI.message("Updating paywall-preview-resources repo to #{commit}")
      Dir.chdir(PAYWALL_PREVIEW_RESOURCES_REPO_NAME) do
        sh "git", "fetch", "origin"
        sh "git", "checkout", commit
      end
    end
  end

  desc "Updates purchases-ios-snapshots-commit to point to latest commit on main"
  lane :update_snapshots_repo do
    new_hash = ""

    Dir.chdir("../Tests/#{SNAPSHOTS_REPO_NAME}") do
      sh "git", "checkout", "main"
      sh "git", "pull", "--rebase", "origin"
      new_hash = sh("git", "rev-parse", "HEAD")
    end

    UI.important("Updating snapshot commit to #{new_hash}")
    update_snapshots_commit(new_hash)
  end

  lane :deploy_to_spm do
    source_repo = ENV["CIRCLE_REPOSITORY_URL"] || "https://github.com/revenuecat/#{REPO_NAME}"
    destination_repo = source_repo.sub(/\.git\z/, "-spm.git")

    git_clone_and_push(
      source_repo: source_repo,
      destination_repo: destination_repo
    )
  end

  desc "Create or delete sandbox testers"
  lane :sandbox_testers do
    Spaceship::ConnectAPI.login(use_portal: false)

    UI.deprecated("Email pattern will be used when creating and deleting multiple testers")
    UI.deprecated("fastlane will increment a number before '@'")
    UI.deprecated("Example: testparty.v4@revenuecat.com will make: ")
    UI.deprecated("   testparty.v4.1@revenuecat.com")
    UI.deprecated("   testparty.v4.2@revenuecat.com")
    UI.deprecated("   testparty.v4.3@revenuecat.com")
    email_pattern = UI.input("Sanbox Tester Email pattern:")

    options = ["Create", "Delete"]
    option = UI.select("What do you want to do?", options)

    if option == options[0]
      sandbox_testers_create(email_pattern: email_pattern)
    elsif option == options[1]
      sandbox_testers_delete(email_pattern: email_pattern)
    end
  end

  desc "Enable customer center development by cherry-picking a specific commit"
  lane :enable_customer_center do |options|
    commit_hash = options[:commit_hash] || "517ad0bf5"

    UI.message("Applying changes from commit to enable customer center: #{commit_hash}")

    begin
      sh("git", "cherry-pick", "--no-commit", commit_hash)
      UI.success("Successfully applied changes for customer center development from commit #{commit_hash}")
      UI.important("Changes are now in your working directory. Review and stage/commit as needed.")
    rescue => ex
      UI.error("Failed to enable customer center. Error while applying changes from commit #{commit_hash}")
      UI.error(ex.message)

      if UI.confirm("Do you want to abort the customer center enablement?")
        sh("git", "cherry-pick", "--abort")
        UI.message("Customer center enablement aborted")
      end
    end
  end

  private_lane :sandbox_testers_delete do |options|
    email_pattern = options[:email_pattern]

    email_prefix = email_pattern.split('@').first

    testers = Spaceship::ConnectAPI::SandboxTester.all.select do |tester|
      tester.email.start_with?(email_prefix)
    end

    UI.important("Found testers:")
    testers.each do |tester|
      UI.message(tester.email)
    end

    if UI.confirm("Do you want to delete these sandbox testers?")
      testers.each do |tester|
        UI.message("Deleting #{tester.email}...")
        tester.delete!
      end
    end
  end

  private_lane :sandbox_testers_create do |options|
    email_pattern = options[:email_pattern]

    number = UI.input("Number of testers:").to_i
    UI.user_error!("Number needs to be more than 0") if number <= 0
    UI.user_error!("Number needs to be less than 51") if number > 50

    password = UI.input("Sandbox Tester Password (ex: 'SuperSecret01'):")

    question = UI.input("Sandbox Tester Secret Question (ex: 'Secret'):")
    answer = UI.input("Sandbox Tester Secret Answer (ex: 'Answer'):")

    emails = (1..number).map do |i|
      email_pattern.gsub('@', ".#{i.to_s}@")
    end

    emails.each do |email|
      data = {
        first_name: "Tester",
        last_name: "FastlaneCreated",
        email: email,
        password: password,
        confirm_password: password,
        secret_question: question,
        secret_answer: answer,
        birth_date: "1990-01-01",
        app_store_territory: "USA"
      }
      UI.message("Creating #{email}...")
      Spaceship::ConnectAPI::SandboxTester.create(**data)
    end
  end

end

platform :mac do
  desc "Runs tests on macOS"
  lane :test_macos do |options|
    generate_snapshots = ENV["CIRCLECI_TESTS_GENERATE_SNAPSHOTS"] == "true"
    if generate_snapshots
      test_plan = "CI-RevenueCat-Snapshots"
    else
      test_plan = "CI-RevenueCat"
    end

    begin
      xcodebuild(
        workspace: 'RevenueCat.xcworkspace',
        scheme: 'RevenueCat',
        destination: "platform=macOS,arch=arm64",
        output_style: :basic,
        result_bundle_path: 'fastlane/test_output/xctest/macos/revenuecat.xcresult',
        report_formats: [:junit],
        report_path: 'fastlane/test_output/xctest/macos/revenuecat/tests.xml',
        test: true,
        xcargs: "-testPlan #{test_plan} CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO"
      )
    rescue => e
      # Equivalent to `fail_build: !generate_snapshots`
      raise e if !generate_snapshots
    end
  end

end

def check_no_git_tag_exists(version_number)
  if git_tag_exists(tag: version_number, remote: true, remote_name: 'origin')
    raise "git tag with version #{version_number} already exists!"
  else
    UI.message("✅ git tag with version #{version_number} doesn't exist")
  end
end

def check_no_github_release_exists(version_number)
  found_release_number = get_github_release(url: "revenuecat/purchases-ios", version: version_number)
  raise "Release with version #{version_number} already exists!" unless found_release_number.nil?
end

lane :check_pods do
  pod_lib_lint(
    verbose: true,
    podspec: 'RevenueCat.podspec',
    platforms: 'ios,osx,tvos,watchos,visionos',
    fail_fast: true
  )
  pod_lib_lint(
    verbose: true,
    podspec: 'RevenueCatUI.podspec',
    include_podspecs: 'RevenueCat.podspec',
    platforms: 'ios,osx,tvos,watchos,visionos',
    fail_fast: true
  )
end

desc "Removes the Examples directory before building with Carthage to prevent unnecessary or problematic scheme resolution (e.g., Magic Weather SwiftUI.xcodeproj)"
lane :prepare_for_carthage do
  Dir.chdir("#{File.dirname(__FILE__)}/../") do
    sh("rm", "-rf", "Examples")
  end
end

lane :load_spm_dependencies do
  Dir.chdir("#{File.dirname(__FILE__)}/../") do
    sh("xcodebuild", "-list")
  end
end

# Snapshots

def snapshots_commit_file
  "#{File.dirname(__FILE__)}/../Tests/#{SNAPSHOTS_REPO_NAME}-commit"
end

def current_snapshots_commit
  File.read(snapshots_commit_file).strip
end
def update_snapshots_commit(hash)
  File.write(snapshots_commit_file, hash)
end

def paywall_preview_resources_commit_file
  "#{File.dirname(__FILE__)}/../Tests/#{PAYWALL_PREVIEW_RESOURCES_REPO_NAME}-commit"
end
def current_paywall_preview_resources_commit
  File.read(paywall_preview_resources_commit_file).strip
end
def update_paywall_preview_resources_commit(hash)
  File.write(paywall_preview_resources_commit_file, hash)
end